package mya_dc.master_server.database;

import java.awt.TrayIcon;
import java.io.Serializable;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Set;
import java.util.Map.Entry;

import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

import mya_dc.master_server.MasterServerGUI;
import mya_dc.shared_classes.ConnectInfo;


/**
 * The compilation servers' DB
 * 
 * @author      Adam Levi
 * <br>			MYA
 */
public class ServersHashTable implements Serializable, TableModel
{
	
	private static final long serialVersionUID = 1L;

	//note: m_ServerCounter starts from 1, zero = error;
	
	/**
	 * Constructor
	 * 
	 * @param serverCounter - the id that the first server to connect will get
	 * @param ParentGUI - the master's GUI
	 * 
	 */
	public ServersHashTable(long serverCounter, MasterServerGUI ParentGUI)
	{
		m_ParentGUI = ParentGUI;
		m_ConnectedServers 			= new Hashtable<Long, ConnectInfo>();
		m_ClientsOnALLServers 		= new Hashtable<Long,Integer>();
		m_ALLServers				= new ArrayList<Long>();
		m_ServerCounter 			= serverCounter; //0 for errors
	}
	
	/**
	 * Clears the hash-table
	 */
	public synchronized void clearRegisteredServers()
	{
		m_ConnectedServers.clear();
	}
	
	
	/**
	 * updates the DB according to the given hash-table. The method will 
	 * take all the data from the given hash-table and it will replace each
	 * entry in m_ClientsOnALLServers with the one from serverIDtoClientsNumber
	 * if serverIDtoClientsNumber doesn't contain an entry that exists
	 * in m_ClientsOnALLServers, the entry in m_ClientsOnALLServers will
	 * be set to zero.
	 * 
	 * @param serverIDtoClientsNumber - an array of serverID to the number of clients
	 * registered at the server
	 * 
	 */
	public synchronized void updateData(Hashtable<Long, Integer> serverIDtoClientsNumber)
	{
		Set<Entry<Long, Integer>> old_clients_hash = m_ClientsOnALLServers.entrySet();
		LinkedList<Long> IDs = new LinkedList<Long>();
		for (Entry<Long, Integer> entry : old_clients_hash)
		{
			IDs.add(entry.getKey());
		}
		m_ClientsOnALLServers.clear();
		for (Long serverID : IDs)
			m_ClientsOnALLServers.put(serverID, 0);
		
		Set<Entry<Long, Integer>> set = serverIDtoClientsNumber.entrySet();
		for (Entry<Long, Integer> entry : set)
		{
			if (!m_ALLServers.contains(entry.getKey()))
				m_ALLServers.add(entry.getKey());
			if (m_ClientsOnALLServers.get(entry.getKey())!=null)
			{
				m_ClientsOnALLServers.remove(entry.getKey());
			}
			m_ClientsOnALLServers.put(entry.getKey(), entry.getValue());
		}
	}
	
	/**
	 * Sets the GUI parent to  parent
	 */
	public synchronized void setGUIParent(MasterServerGUI parent)
	{
		m_ParentGUI = parent;
	}
	
	/**
	 * Registers a new server at the hash table
	 * 
	 * @param address - The server's address
	 * 
	 * @return The server's ID
	 */
	public synchronized long registerServer(InetAddress address) //returns the server number
	{
		if (address == null)
			return 0; //error
		long ID = getServerID();
		m_ClientsOnALLServers.put(ID, 0);		
		m_ALLServers.add(ID);
		return m_ServerCounter++;
	}
	
	/**
	 * Registers an existing server
	 * 
	 * @param connInfo - The connection information for the server
	 * @param serverNumber - The server's ID
	 * 
	 * @return -1 if a server with the same ID is already connected (error), 0 otherwise
	 */
	public synchronized int registerServer(ConnectInfo connInfo, long serverNumber)
	{
		if (!m_ALLServers.contains(serverNumber))
			m_ALLServers.add(serverNumber);
		if (m_ConnectedServers.containsKey(serverNumber))
			return -1;
		// checks if the server has clients mapped to it: 
		// note: if data is consistent the following expression 
		// should return true
		if (!(m_ClientsOnALLServers.containsKey(serverNumber)))
			m_ClientsOnALLServers.put(serverNumber,0);
		m_ConnectedServers.put(serverNumber, connInfo);
		m_ParentGUI.UpdateServersTables();
		m_ParentGUI.SendNotification("Information", "Accepted connection from a Compilation Server with ID " + serverNumber,
				TrayIcon.MessageType.INFO);
		return 0;
	}
	
	/**
	 * Unregisters a server from the hash table (marks a connected server
	 * as disconnected)
	 * 
	 * @param serverNumber - The server's ID
	 * 
	 */
	public synchronized void unregisterServer(long serverNumber)
	{
		if (!m_ConnectedServers.containsKey(serverNumber))
			return;
		m_ConnectedServers.remove(serverNumber);
		m_ParentGUI.UpdateServersTables();
		m_ParentGUI.SendNotification("Information", "Connection terminated with Compilation Server ID " + serverNumber,
				TrayIcon.MessageType.INFO);
	}
	
	/**
	 * returns the Connection Information of a specific server
	 * 
	 * @param serverNumber - The server's ID
	 * 
	 * @return a ConnectInfo for this server if the server is connected, otherwise null
	 */
	public synchronized ConnectInfo getServer(long serverNumber)
	{
		return m_ConnectedServers.get(serverNumber);
	}
	
	/**
	 * returns a sorted list of servers (sorted from the one that has
	 * the least of clients connected to, to the one that has the most
	 * number of clients connected).
	 * 
	 * @param serverNumber - The server's ID
	 * 
	 * @return an array of objects of type ServerLoad
	 */
	public synchronized Object[] getConnectedServers()
	{
		
		Set<Entry<Long, Integer>> set = m_ClientsOnALLServers.entrySet();
		LinkedList<ConnectInfoAndID> list = new LinkedList<ConnectInfoAndID>();
		for (Entry<Long, Integer> entry : set)
		{
			//check if connected
			if (m_ConnectedServers.containsKey(entry.getKey()))
			{
				long id = entry.getKey();
				ConnectInfo info = m_ConnectedServers.get(id);
				int clients = entry.getValue();
				list.add(new ConnectInfoAndID(info,id, clients));
			}	
		}
		Object[] arr = list.toArray();
		Arrays.sort(arr);
		return arr;
	}
	
	/**
	 * @param serverID - The server's ID
	 * 
	 * @return true if the given server is connected
	 */
	public synchronized boolean isConnected(Long serverID)
	{
		return m_ConnectedServers.containsKey(serverID);
	}
	
	/**
	 * registers a client to a specific server
	 * 
	 * @param serverNumber - The server's ID
	 * 
	 */
	public synchronized void registerClient(Long serverNumber) 
	{
		boolean m_flag = m_ClientsOnALLServers.containsKey(serverNumber);
		int totalClients = (m_flag ? m_ClientsOnALLServers.get(serverNumber) + 1 : 1);

		//updates m_ClientsOnALLServers
		if (m_flag)
			m_ClientsOnALLServers.remove(serverNumber);
		m_ClientsOnALLServers.put(serverNumber, totalClients);
	}
	
	/**
	 * unregisters a client from a specific server
	 * 
	 * @param serverNumber - The server's ID
	 * 
	 */
	public synchronized void unRegisterClient(Long serverNumber)
	{
		if (!(m_ClientsOnALLServers.containsKey(serverNumber)))
			return;
		int curClientsNumber = m_ClientsOnALLServers.get(serverNumber);
		m_ClientsOnALLServers.remove(serverNumber);
		m_ClientsOnALLServers.put(serverNumber, (curClientsNumber == 0 ? 0 : curClientsNumber - 1));
	}
	
	public transient MasterServerGUI 	m_ParentGUI;
	private ArrayList<Long>				m_ALLServers;
	private Hashtable<Long,ConnectInfo>	m_ConnectedServers;
	private Hashtable<Long,Integer> 	m_ClientsOnALLServers; //map from server's number to number of clients registered
	private long						m_ServerCounter;
	
	
	/**
	 * @return an ID that doesn't appear at m_ALLServers
	 */
	private long getServerID()
	{
		while(m_ALLServers.contains(m_ServerCounter))
		{
			m_ServerCounter++;
		}
		return m_ServerCounter;
	}
	
	/**
	 * A class to handle the number of clients in each server
	 */
	
	public class ConnectInfoAndID implements Serializable, Comparable<ConnectInfoAndID>
	{
		private static final long serialVersionUID = 7966258455462688892L;
		public ConnectInfoAndID(
				ConnectInfo _connectInfo,
				Long _serverID,
				int _clients)
		{
			connectInfo = _connectInfo;
			serverID = _serverID;
			clients = _clients;
		}
		public int clients;
		public ConnectInfo connectInfo;
		public long	serverID;
		@Override
		public int compareTo(ConnectInfoAndID o)
		{
			if (o.clients == this.clients)
				return 0;
			else if 
				((o.clients < this.clients))
				return 1;
			else
				return -1;
		}
	}
	
	/**
	 * 
	 * @param serverNumber - the server's ID
	 * 
	 * @return The listen port for the given server
	 */
	public synchronized int getListenPort(long serverNumber)
	{
		return m_ConnectedServers.get(serverNumber).listenPort;
	}
	
	/* table model implementation */
	
	public static final String[] ColumnName = {"Server ID", "IP Address", "Accepts on Port", "Number of Clients"};
	public static final int ColumnCount = 4;
	private static final String UnAvailableStr = "N/A";
	@Override
	public void addTableModelListener(TableModelListener l) { }
	
	private synchronized Class<?> getColumnClassSync(int columnIndex) {
		return getValueAt(0, columnIndex).getClass();
	}
	
	@Override
	public Class<?> getColumnClass(int columnIndex) {
		return getColumnClassSync(columnIndex);
	}
	
	@Override
	public int getColumnCount() {
		return ServersHashTable.ColumnCount;
	}
	@Override
	public String getColumnName(int columnIndex) {
		return ServersHashTable.ColumnName[columnIndex];
	}
	
	private synchronized int getRowCountSync()
	{
		return this.m_ALLServers.size();
	}
	@Override
	public int getRowCount() {
		return getRowCountSync();
	}
	private Long getServerAt(int rowIndex)
	{
		if (rowIndex < 0 || rowIndex >= m_ALLServers.size())
			return null;
		return m_ALLServers.get(rowIndex);
	}
	
	private synchronized Object getValueAtSync(int rowIndex, int columnIndex)
	{
		Long serverID = getServerAt(rowIndex);
		switch(columnIndex)
		{
			case 0: //Server ID
			{
				return serverID;
			}
			case 1: //IP adrress
			{
				if (m_ConnectedServers.containsKey(serverID))
					return m_ConnectedServers.get(serverID).address;
				else
					return ServersHashTable.UnAvailableStr;
			}
			case 2: //Port
			{
				if (m_ConnectedServers.containsKey(serverID))
					return m_ConnectedServers.get(serverID).listenPort;
				else
					return ServersHashTable.UnAvailableStr;
			}
			case 3:
			{
				return m_ClientsOnALLServers.get(serverID);
			}
			default: return null;
		}	
	}
	
	@Override
	public Object getValueAt(int rowIndex, int columnIndex)
	{
		return getValueAtSync(rowIndex, columnIndex);
	}
	@Override
	public boolean isCellEditable(int rowIndex, int columnIndex) {
		return false;
	}
	@Override
	public void removeTableModelListener(TableModelListener l) { }
	@Override
	public void setValueAt(Object value, int rowIndex, int columnIndex) { }
}
